#!/usr/bin/python
#
# Copyright (c) 2009, Michael J. Wiacek Jr.
# 
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
 
import hashlib
import logging
import simplejson
import struct
import urllib2
import ZenFolioLink


class Error(Exception):
  pass


class RpcError(Error):
  def __init__(self, code=None, message=None):
    Error.__init__(self)
    self.code = code
    self.message = message


def PackParams(*args):
  return simplejson.dumps(args)


def BuildAccessUpdater(access_mask=None, access_type=None,
                       viewers=None, password=None, is_derived=None):
  access_updater = {}
  
  if access_mask is not None:
    if isinstance(access_mask, basestring):
      access_mask = [x.strip() for x in access_mask.split(',')]
    elif isinstance(access_mask, list) or isinstance(access_mask, tuple):
      pass
    else:
      raise ValueError('Unknown type of access_mask specified!')
    
    # Now determine whether access_mask has a sane value.
    if access_mask:
      for mask in access_mask:
        if mask not in ('None', 'HideDateCreated', 'HideDateModified',
                        'HideDateTaken', 'HideMetaData', 'HideUserStats',
                        'HideVisits', 'NoCollections', 'NoPrivateSearch',
                        'NoPublicSearch', 'NoRecentList', 'ProtectExif',
                        'ProtectExtraLarge', 'ProtectLarge', 'ProtectMedium',
                        'ProtectOriginals', 'ProtectGuestbook',
                        'NoPublicGuestbookPosts', 'NoPrivateGuestbookPosts',
                        'NoAnonymousGuestbookPosts', 'ProtectComments',
                        'NoPublicComments', 'NoPrivateComments',
                        'NoAnonymousComments', 'ProtectAll'):
          raise ValueError('Invalid access_mask value specified: %s' % mask)
          
      # If we got this far, access_mask has more than 0 values, all of which are valid.
      access_updater['AccessMask'] = ', '.join(access_mask)

  if access_type is not None:
    if access_type not in ('Private', 'Public', 'UserList', 'Password'):
      raise ValueError('Invalid access_type specified')
    else:
      access_updater['AccessType'] = access_type

  if viewers is not None:
    if isinstance(viewers, list) or isinstance(viewers, tuple):
      access_updater['Viewers'] = viewers
    else:
      raise ValueError('viewers not specified in a list or tuple.')
    
  if password is not None:
    access_updater['Password'] = password

  if is_derived is not None:
    if is_derived == True or is_derived == False:
      access_updater['IsDerived'] = is_derived
    else:
      raise ValueError('is_derived must be True or False')

  return access_updater


def BuildPhotosetUpdater(title=None,
                         caption=None,
                         keywords=None,
                         categories=None,
                         custom_reference=None):
  updater = {}
  if title is not None:
    updater['Title'] = title
  if caption is not None:
    updater['Caption'] = caption
  if keywords is not None and (
      isinstance(keywords, list) or isinstance(keywords, tuple)):
    updater['Keywords'] = keywords
  if categories is not None and (
      isinstance(categories, list) or isinstance(categories, tuple)):
    updater['Categories'] = categories
  if custom_reference is not None:
    updater['CustomReference'] = custom_reference
  return updater


def BuildGroupUpdater(title=None, caption=None, custom_reference=None):
  updater = {}
  if title is not None:
    updater['Title'] = title
  if caption is not None:
    updater['Caption'] = caption
  if custom_reference is not None:
    updater['CustomReference'] = custom_reference
  return updater


def BuildPhotoUpdater(title=None,
                      caption=None,
                      keywords=None,
                      categories=None,
                      copyright=None,
                      filename=None):
  updater = {}
  if title is not None:
    updater['Title'] = title
  if caption is not None:
    updater['Caption'] = caption
  if keywords is not None and (
      isinstance(keywords, list) or isinstance(keywords, tuple)):
    updater['Keywords'] = keywords
  if categories is not None and (
      isinstance(categories, list) or isinstance(categories, tuple)):
    updater['Categories'] = categories
  if copyright is not None:
    updater['Copyright'] = copyright
  if filename is not None:
    updater['Filename'] = filename
  return updater


def Call(method, auth=None, use_ssl=False, params=None):
  if params is None:
    params = '[]'

  try:
    resp = ZenFolioLink.MakeRequest(
        method, params, auth, use_ssl)
  except ZenFolioLink.HttpError, e:
    logging.warning('ZenFolio API Call for %s failed with params: %s\n'
                    'response code %d with body:\n %s', method, params,
                    e.code, e.body)
    raise Error
  else:
    response = resp.read()
    rpc_obj = simplejson.loads(response)
    if rpc_obj['error'] is None:
      return rpc_obj['result']
    else:
      if 'code' in rpc_obj['error']:
        raise RpcError(
            message=rpc_obj['error']['message'], code=rpc_obj['error']['code'])
      else:
        raise RpcError(message=rpc_obj['error']['message'])


def LoadPhoto(photo_id, auth=None ):
  return Call(
      'LoadPhoto', auth=auth, params=PackParams(photo_id))


def LoadPhotoSet(photoset_id, auth=None):
  return Call('LoadPhotoSet',
              auth=auth,
              params=PackParams(photoset_id))


def Authenticate(auth_challenge, password):
  salt = ''.join([chr(x) for x in auth_challenge['PasswordSalt']])
  challenge = ''.join([chr(x) for x in auth_challenge['Challenge']])

  combo = salt + password.encode('utf-8')
  h2 = hashlib.sha256(combo)
  combo = challenge + h2.digest()
  h1 = hashlib.sha256(combo)
  proof = h1.digest()

  byte_proof = struct.unpack('B'*32, proof)

  try:
    resp = Call('Authenticate',
                use_ssl=True,
                params=PackParams(auth_challenge['Challenge'], byte_proof))
  except RpcError, e:
    logging.warning(
        'Authentication failed code: %s and message: %s', e.code, e.message)
    return None
  else:
    return resp


def GetChallenge(username):
  return Call('GetChallenge', params=PackParams(username))


def AuthenticatePlain(username, password):
  return Call(
      'AuthenticatePlain', use_ssl=True, params=PackParams(username, password))


def AddPhotoToCollection(photo_id, collection_id, auth):
  return Call('CollectionAddPhoto',
              auth=auth,
              params=PackParams(collection_id, photo_id))


def RemovePhotoFromCollection(photo_id, collection_id, auth):
  return Call('CollectionRemovePhoto',
              auth=auth,
              params=PackParams(collection_id, photo_id))


def CreateGroup(parent_id, group_title=None, group_caption=None, custom_reference=None, auth=None):
  updater = BuildGroupUpdater(group_title=group_title,
                              group_caption=group_caption,
                              custom_reference=custom_reference)
  return Call('CreateGroup',
              auth=auth,
              params = PackParams(parent_id, updater))


def CreatePhotoset(parent_id, photoset_type=None, title=None, caption=None,
                   keywords=None, categories=None, custom_reference=None, 
                   auth=None):
  if photoset_type not in ('Gallery', 'Collection'):
    raise ValueError('photoset type has invalid valid: %s' % photoset_type)

  updater = BuildPhotosetUpdater(title=title, caption=caption,
                                 keywords=keywords, categories=categories,
                                 custom_reference=custom_reference)
 
  return Call('CreatePhotoSet',
              auth=auth,
              params=PackParams(parent_id, photoset_type, updater))


def DeleteGroup(group_id, auth):
  return Call('DeleteGroup', auth=auth, params=PackParams(group_id))


def DeletePhoto(photo_id, auth):
  return Call('DeletePhoto', auth=auth, params=PackParams(photo_id))


def DeletePhotoset(photoset_id, auth):
  return Call('DeletePhotoSet', auth=auth, params=PackParams(photoset_id))


def GetCategories():
  return Call('GetCategories', use_ssl=False)


def GetPopularPhotos(offset=0, limit=0, auth=None):
  return Call('GetPopularPhotos',
              auth=auth,
              params=PackParams(offset, limit))


def GetPopularSets(photoset_type=None, offset=0, limit=15, auth=None):
  if photoset_type not in ('Gallery', 'Collection'):
    raise ValueError('photoset type has invalid valid: %s' % photoset_type)

  return Call('GetPopularSets',
              auth=auth,
              params=PackParams(photoset_type, offset, limit))


def GetRecentPhotos(offset=0, limit=15, auth=None):
  return Call('GetRecentPhotos',
              auth=auth,
              params=PackParams(offset, limit))

def GetRecentSets(photoset_type=None, offset=0, limit=15, auth=None):
  if photoset_type not in ('Gallery', 'Collection'):
    raise ValueError('photoset type has invalid valid: %s' % photoset_type)

  return Call('GetRecentSets',
              auth=auth,
              params=PackParams(photoset_type, offset, limit))


def KeyringAddKeyPlain(keyring=None, realmId=None, password=None, auth=None):
  return Call('KeyringAddKeyPlain',
              auth=auth,
              use_ssl=True,
              params=PackParams(keyring, realmId, password))


def LoadGroup(group_id, auth=None):
  return Call('LoadGroup',
              auth=auth,
              params=PackParams(group_id))


def LoadGroupHierarchy(username, auth=None):
  return Call('LoadGroupHierarchy',
              auth=auth,
              params=PackParams(username))


def LoadPhoto(photo_id, auth=None):
  return Call('LoadPhoto',
              auth=auth,
              params=PackParams(photo_id))


def LoadPhotoSet(photoset_id, auth=None):
  return Call('LoadPhotoSet',
              auth=auth,
              params=PackParams(photoset_id))


def LoadPrivateProfile(auth):
  return Call('LoadPrivateProfile', auth=auth)


def LoadPublicProfile(username, auth=None):
  return Call('LoadPublicProfile', auth=auth, params=PackParams(username))


def MoveGroup(group_id, dest_group_id, index, auth):
  return Call('MoveGroup',
              auth=auth, 
              params=PackParams(group_id, dest_group_id, index))


def MovePhoto(src_set_id, photo_id, dest_set_id, index, auth):
  return Call('MovePhoto',
              auth=auth,
              params=PackParams(src_set_id, photo_id, dest_set_id, index))


def MovePhotoSet(photoset_id, dest_group_id, index, auth):
  return Call('MovePhotoSet',
              auth=auth, 
              params=PackParams(photoset_id, dest_group_id, index))


def ReorderGroup(group_id, group_shift_order, auth):
  if group_shift_order not in ('CreatedAsc', 'CreatedDesc', 'ModifiedAsc',
                               'ModifiedDesc', 'TitleAsc', 'TitleDesc',
                               'GroupsTop', 'GroupsBottom'):
    raise ValueError('group shift order is invalid!')

  return Call('ReorderGroup',
              auth=auth,
              params=PackParams(group_id, group_shift_order))


def ReorderPhotoSet(photoset_id, shift_order, auth):
  if shift_order not in ('CreatedAsc', 'CreatedDesc', 'TakenAsc',
                         'TakenDesc', 'TitleAsc', 'TitleDesc', 'SizeAsc',
                         'SizeDesc', 'FileNameAsc', 'FileNameDesc'):
    raise ValueError('shift order is invalid!')

  return Call('ReorderPhotoSet',
              auth=auth,
              params=PackParams(photoset_id, shift_order))


def ReplacePhoto(original_photo_id, replacement_photo_id, auth):
  return Call('ReplacePhoto',
              auth=auth,
              params=PackParams(original_photo_id, replacement_photo_id))


def RotatePhoto(photo_id, photo_rotation, auth):

  if photo_rotation not in ('None', 'Rotate90', 'Rotate180', 'Rotate270',
                            'Flip', 'Rotate90Flip', 'Rotate180Flip',
                            'Rotate270Flip'):
    raise ValueError('photo_rotation order is invalid!')

  return Call('RotatePhoto',
              auth=auth,
              params=PackParams(photo_id, photo_rotation))


def SearchPhotoByCategory(search_id=None,
                          sort_order=None,
                          category_code=None,
                          offset=0,
                          limit=15,
                          auth=None):
  if sort_order not in ('Date', 'Popularity'):  # Rank is not applicable
    if sort_order is not None:
      raise ValueError('sort_order is not valid!')

  return Call('SearchPhotoByCategory',
              auth=auth,
              params=PackParams(search_id,
                                sort_order,
                                category_code,
                                offset,
                                limit))


def SearchPhotoByText(search_id=None,
                      sort_order=None,
                      query=None,
                      offset=0,
                      limit=15,
                      auth=None):
  if sort_order not in ('Date', 'Popularity', 'Rank'):
    if sort_order is not None:
      raise ValueError('sort_order is not valid!')

  return Call('SearchPhotoByText',
              auth=auth,
              params=PackParams(search_id,
                                sort_order,
                                query,
                                offset,
                                limit))


def SearchSetByCategory(search_id=None,
                        photoset_type=None,
                        sort_order=None,
                        category_code=None,
                        offset=0,
                        limit=15,
                        auth=None):
  if sort_order not in ('Date', 'Popularity'):  # Rank is not applicable
    if sort_order is not None:
      raise ValueError('sort_order is not valid!')

  if photoset_type not in ('Gallery', 'Collection'):
    raise ValueError('photoset type has invalid valid: %s' % photoset_type)

  return Call('SearchSetByCategory',
              auth=auth,
              params=PackParams(search_id,
                                photoset_type,
                                sort_order,
                                category_code,
                                offset,
                                limit))


def SearchSetByText(search_id=None,
                    photoset_type=None,
                    sort_order=None,
                    query=None,
                    offset=0,
                    limit=15,
                    auth=None):
  if sort_order not in ('Date', 'Popularity', 'Rank'):
    if sort_order is not None:
      raise ValueError('sort_order is not valid!')

  return Call('SearchSetByText',
              auth=auth,
              params=PackParams(search_id,
                                photoset_type,
                                sort_order,
                                query,
                                offset,
                                limit))


def SetGroupTitlePhoto(group_id, photo_id, auth):
  return Call('SetGroupTitlePhoto',
              auth=auth,
              params=PackParams(group_id, photo_id))


def SetPhotoSetFeaturedIndex(photoset_id, index, auth):
  return Call('SetPhotoSetFeaturedIndex',
              auth=auth,
              params=PackParams(photoset_id, index))


def SetPhotoSetTitlePhoto(photoset_id, photo_id, auth):
  return Call('SetPhotoSetTitlePhoto',
              auth=auth,
              params=PackParams(photoset_id, photo_id))


def UpdateGroup(group_id,
                group_title=None,
                group_caption=None,
                custom_reference=None,
                auth=None):
  updater = BuildGroupUpdater(group_title=group_title,
                              group_caption=group_caption,
                              custom_reference=custom_reference)
  return Call('UpdateGroup',
              auth=auth,
              params=PackParams(group_id, updater))


def UpdatePhoto(photo_id,
                title=None,
                caption=None,
                keywords=None,
                categories=None,
                copyright=None,
                filename=None,
                auth=None):
  updater = BuildPhotoUpdater(title=title,
                              caption=caption,
                              keywords=keywords,
                              categories=categories,
                              copyright=copyright,
                              filename=filename)

  return Call('UpdatePhoto',
              auth=auth,
              params=PackParams(photo_id, updater))


def UpdatePhotoSet(photoset_id,
                   title=None,
                   caption=None,
                   keywords=None,
                   categories=None,
                   custom_reference=None,
                   auth=None):
  updater= BuildPhotosetUpdater(title=title,
                                caption=caption,
                                keywords=keywords,
                                categories=categories,
                                custom_reference=custom_reference)

  return Call('UpdatePhotoSet',
              auth=auth,
              params=PackParams(photoset_id, updater))


def UpdateGroupAccess(group_id, access_mask=None, access_type=None,
                      viewers=None, password=None, is_derived=None, auth=None):
  access_updater = BuildAccessUpdater(access_mask, access_type,
                                      viewers, password, is_derived)
  return Call('UpdateGroupAccess',
              auth=auth,
              params=PackParams(group_id, access_updater))


def UpdatePhotoAccess(photo_id, access_mask=None, access_type=None,
                      viewers=None, password=None, is_derived=None, auth=None):
  access_updater = BuildAccessUpdater(access_mask, access_type,
                                      viewers, password, is_derived)
  return Call('UpdatePhotoAccess',
              auth=auth,
              params=PackParams(photo_id, access_updater))


def UpdatePhotoSetAccess(photoset_id, access_mask=None, access_type=None,
                         viewers=None, password=None, is_derived=None,
                         auth=None):
  access_updater = BuildAccessUpdater(access_mask, access_type,
                                      viewers, password, is_derived)
  return Call('UpdatePhotoSetAccess',
              auth=auth,
              params=PackParams(photoset_id, access_updater))
